package net.aiion.weave.impl.ui.transformation;

import java.util.ArrayList;
import java.util.Collection;

import net.aiion.weave.asm.Label;
import net.aiion.weave.asm.Opcodes;
import net.aiion.weave.asm.Type;
import net.aiion.weave.impl.ui.binder.BinderException;
import net.aiion.weave.spi.bytecode.Bytecode;
import net.aiion.weave.spi.bytecode.BytecodeContext;
import net.aiion.weave.spi.bytecode.BytecodeException;
import net.aiion.weave.spi.bytecode.BytecodeGenerator;
import net.aiion.weave.spi.bytecode.events.AnnotationEndBytecodeEvent;
import net.aiion.weave.spi.bytecode.events.AnnotationStartBytecodeEvent;
import net.aiion.weave.spi.bytecode.events.AnnotationValueBytecodeEvent;
import net.aiion.weave.spi.bytecode.events.BytecodeEvent;
import net.aiion.weave.spi.bytecode.events.ClassEndBytecodeEvent;
import net.aiion.weave.spi.bytecode.events.ClassStartBytecodeEvent;
import net.aiion.weave.spi.bytecode.events.FieldEndBytecodeEvent;
import net.aiion.weave.spi.bytecode.events.FieldStartBytecodeEvent;
import net.aiion.weave.spi.bytecode.events.MethodEndBytecodeEvent;
import net.aiion.weave.spi.bytecode.events.MethodStartBytecodeEvent;
import net.aiion.weave.spi.bytecode.util.BytecodeTransformation;
import net.aiion.weave.spi.naming.annotations.Named;
import net.aiion.weave.spi.ui.Bindable;
import net.aiion.weave.spi.ui.annotations.Binds;
import net.aiion.weave.spi.ui.annotations.BindsContent;

public class BindableTransformation extends BytecodeTransformation {
    private Collection<BytecodeEvent> getValueTypeBytecodeEvents;
    private BytecodeGenerator getValueTypeBytecodeGenerator;

    private Collection<BytecodeEvent> bindValueBytecodeEvents;
    private BytecodeGenerator bindValueBytecodeGenerator;

    private String className;

    private String fieldName;
    private String fieldDescriptor;

    private String methodName;
    private String methodDescriptor;

    private String bindsName;

    private boolean hasBindsAnnotation;
    private boolean inBindsAnnotation;

    private boolean hasBindsContentAnnotation;

    public BindableTransformation() {
    }

    @Override
    public boolean testRetain(final BytecodeContext aBytecodeContext, final BytecodeEvent aBytecodeEvent) {
        return aBytecodeEvent.getType() == BytecodeEvent.CLASS_START
            && !aBytecodeEvent.asClassStart().containsInterface(Bindable.class);
    }

    @Override
    public boolean testProcess(final BytecodeContext aBytecodeContext, final BytecodeEvent aBytecodeEvent) {
        return aBytecodeEvent.getType() == BytecodeEvent.ANNOTATION_START
            && aBytecodeEvent.asAnnotationStart().equalsType(Named.class);
    }

    @Override
    public boolean testSkip(final BytecodeContext aBytecodeContext, final BytecodeEvent aBytecodeEvent) {
        return aBytecodeEvent.getType() == BytecodeEvent.CLASS_START
            && (aBytecodeEvent.asClassStart().isInterface() || aBytecodeEvent.asClassStart().containsInterface(
                Bindable.class));
    }

    @Override
    public void handleBytecodeEvent(final BytecodeContext aBytecodeContext, final BytecodeEvent aBytecodeEvent) {
        switch (aBytecodeEvent.getType()) {
        case BytecodeEvent.CLASS_START:
            handleClassStartBytecodeEvent(aBytecodeContext, aBytecodeEvent.asClassStart());
            break;
        case BytecodeEvent.ANNOTATION_START:
            handleAnnotationStartBytecodeEvent(aBytecodeContext, aBytecodeEvent.asAnnotationStart());
            break;
        case BytecodeEvent.ANNOTATION_VALUE:
            handleAnnotationValueBytecodeEvent(aBytecodeContext, aBytecodeEvent.asAnnotationValue());
            break;
        case BytecodeEvent.ANNOTATION_END:
            handleAnnotationEndBytecodeEvent(aBytecodeContext, aBytecodeEvent.asAnnotationEnd());
            break;
        case BytecodeEvent.FIELD_START:
            handleFieldStartBytecodeEvent(aBytecodeContext, aBytecodeEvent.asFieldStart());
            break;
        case BytecodeEvent.FIELD_END:
            handleFieldEndBytecodeEvent(aBytecodeContext, aBytecodeEvent.asFieldEnd());
            break;
        case BytecodeEvent.METHOD_START:
            handleMethodStartBytecodeEvent(aBytecodeContext, aBytecodeEvent.asMethodStart());
            break;
        case BytecodeEvent.METHOD_END:
            handleMethodEndBytecodeEvent(aBytecodeContext, aBytecodeEvent.asMethodEnd());
            break;
        case BytecodeEvent.CLASS_END:
            handleClassEndBytecodeEvent(aBytecodeContext, aBytecodeEvent.asClassEnd());
            break;
        }

        aBytecodeContext.forwardBytecodeEvent(aBytecodeEvent);
    }

    private void handleClassStartBytecodeEvent(final BytecodeContext aBytecodeContext,
        final ClassStartBytecodeEvent aClassStartBytecodeEvent) {
        className = aClassStartBytecodeEvent.getName();

        aClassStartBytecodeEvent.addInterface(Bindable.class);

        prepareGetValueType();
        prepareBindValue();
    }

    private void handleAnnotationStartBytecodeEvent(final BytecodeContext aBytecodeContext,
        final AnnotationStartBytecodeEvent aAnnotationStartBytecodeEvent) {
        if (aAnnotationStartBytecodeEvent.equalsType(Binds.class)) {
            hasBindsAnnotation = true;
            inBindsAnnotation = true;
        }

        if (aAnnotationStartBytecodeEvent.equalsType(BindsContent.class)) {
            hasBindsContentAnnotation = true;
            bindsName = "";
        }
    }

    private void handleAnnotationValueBytecodeEvent(final BytecodeContext aBytecodeContext,
        final AnnotationValueBytecodeEvent aAnnotationValueBytecodeEvent) {
        if (inBindsAnnotation && "value".equals(aAnnotationValueBytecodeEvent.getName())) {
            bindsName = (String) aAnnotationValueBytecodeEvent.getValue();
        }
    }

    private void handleAnnotationEndBytecodeEvent(final BytecodeContext aBytecodeContext,
        final AnnotationEndBytecodeEvent aAnnotationEndBytecodeEvent) {
        if (inBindsAnnotation) {
            validateBindsField();
            validateBindsMethod();
            inBindsAnnotation = false;
        }
    }

    private void validateBindsField() {
        if (fieldName == null) {
            return;
        }
    }

    private void validateBindsMethod() {
        if (methodName == null) {
            return;
        }

        final Type[] argumentTypes = Type.getArgumentTypes(methodDescriptor);
        if (argumentTypes.length != 1) {
            throw new BytecodeException(String.format(
                "@Binds annotated method %s.%s must return void and accept exactly one argument of any type.",
                className.replace('/', '.'), methodName));
        }
    }

    private void handleFieldStartBytecodeEvent(final BytecodeContext aBytecodeContext,
        final FieldStartBytecodeEvent aFieldStartBytecodeEvent) {
        fieldName = aFieldStartBytecodeEvent.getName();
        fieldDescriptor = aFieldStartBytecodeEvent.getDescriptor();
    }

    private void handleFieldEndBytecodeEvent(final BytecodeContext aBytecodeContext,
        final FieldEndBytecodeEvent aFieldEndBytecodeEvent) {
        if (hasBindsAnnotation) {
            extendGetValueTypeForField();
            extendBindValueForField();
        }

        if (hasBindsContentAnnotation) {
            extendBindValueForField();
        }

        fieldName = null;
        bindsName = null;
        hasBindsAnnotation = false;
        hasBindsContentAnnotation = false;
    }

    private void handleMethodStartBytecodeEvent(final BytecodeContext aBytecodeContext,
        final MethodStartBytecodeEvent aMethodStartBytecodeEvent) {
        methodName = aMethodStartBytecodeEvent.getName();
        methodDescriptor = aMethodStartBytecodeEvent.getDescriptor();
    }

    private void handleMethodEndBytecodeEvent(final BytecodeContext aBytecodeContext,
        final MethodEndBytecodeEvent aMethodEndBytecodeEvent) {
        if (hasBindsAnnotation) {
            extendGetValueTypeForMethod();
            extendBindValueForMethod();
        }

        if (hasBindsContentAnnotation) {
            extendBindValueForMethod();
        }

        methodName = null;
        bindsName = null;
        hasBindsAnnotation = false;
        hasBindsContentAnnotation = false;
    }

    private void handleClassEndBytecodeEvent(final BytecodeContext aBytecodeContext,
        final ClassEndBytecodeEvent aClassEndBytecodeEvent) {
        finishGetValueType();
        finishBindValue();

        aBytecodeContext.forwardBytecodeEvents(getValueTypeBytecodeEvents);
        aBytecodeContext.forwardBytecodeEvents(bindValueBytecodeEvents);
    }

    private void prepareGetValueType() {
        getValueTypeBytecodeEvents = new ArrayList<>();
        getValueTypeBytecodeGenerator = Bytecode.createGenerator(getValueTypeBytecodeEvents);

        getValueTypeBytecodeGenerator.startMethod(Opcodes.ACC_PUBLIC, "getValueType", Class.class, String.class);
        getValueTypeBytecodeGenerator.startCode();
    }

    private void extendGetValueTypeForField() {
        final Label nextNameLabel = new Label();

        getValueTypeBytecodeGenerator.pushConstant(bindsName);
        getValueTypeBytecodeGenerator.loadReference(1);
        getValueTypeBytecodeGenerator.invokeVirtual(String.class, "equals", boolean.class, Object.class);
        getValueTypeBytecodeGenerator.jumpEquals(nextNameLabel);

        getValueTypeBytecodeGenerator.pushConstant(Type.getType(fieldDescriptor));
        getValueTypeBytecodeGenerator.returnReference();

        getValueTypeBytecodeGenerator.markLabel(nextNameLabel);
        getValueTypeBytecodeGenerator.markFrameSame();
    }

    private void extendGetValueTypeForMethod() {
        final Label nextNameLabel = new Label();

        getValueTypeBytecodeGenerator.pushConstant(bindsName);
        getValueTypeBytecodeGenerator.loadReference(1);
        getValueTypeBytecodeGenerator.invokeVirtual(String.class, "equals", boolean.class, Object.class);
        getValueTypeBytecodeGenerator.jumpEquals(nextNameLabel);

        getValueTypeBytecodeGenerator.pushConstant(Type.getReturnType(methodDescriptor));
        getValueTypeBytecodeGenerator.returnReference();

        getValueTypeBytecodeGenerator.markLabel(nextNameLabel);
        getValueTypeBytecodeGenerator.markFrameSame();
    }

    private void finishGetValueType() {
        getValueTypeBytecodeGenerator.newReference(BinderException.class);
        getValueTypeBytecodeGenerator.duplicateReference();

        getValueTypeBytecodeGenerator.newReference(StringBuilder.class);
        getValueTypeBytecodeGenerator.duplicateReference();
        getValueTypeBytecodeGenerator.pushConstant("There is no bindable property with name '");
        getValueTypeBytecodeGenerator.invokeConstructor(StringBuilder.class, String.class);
        getValueTypeBytecodeGenerator.loadReference(1);
        getValueTypeBytecodeGenerator.invokeVirtual(StringBuilder.class, "append", StringBuilder.class, String.class);
        getValueTypeBytecodeGenerator.pushConstant(String.format("' in class '%s'.", className.replace('/', '.')));
        getValueTypeBytecodeGenerator.invokeVirtual(StringBuilder.class, "append", StringBuilder.class, String.class);
        getValueTypeBytecodeGenerator.invokeVirtual(StringBuilder.class, "toString", String.class);

        getValueTypeBytecodeGenerator.invokeConstructor(BinderException.class, String.class);
        getValueTypeBytecodeGenerator.throwReference();

        getValueTypeBytecodeGenerator.endCode(5, 2);
        getValueTypeBytecodeGenerator.endMethod();
    }

    private void prepareBindValue() {
        bindValueBytecodeEvents = new ArrayList<>();
        bindValueBytecodeGenerator = Bytecode.createGenerator(bindValueBytecodeEvents);

        bindValueBytecodeGenerator.startMethod(Opcodes.ACC_PUBLIC, "bindValue", void.class, String.class, Object.class);
        bindValueBytecodeGenerator.startCode();
    }

    private void extendBindValueForField() {
        final Label nextNameLabel = new Label();

        bindValueBytecodeGenerator.pushConstant(bindsName);
        bindValueBytecodeGenerator.loadReference(1);
        bindValueBytecodeGenerator.invokeVirtual(String.class, "equals", boolean.class, Object.class);
        bindValueBytecodeGenerator.jumpEquals(nextNameLabel);

        bindValueBytecodeGenerator.loadReference(0);
        bindValueBytecodeGenerator.loadReference(2);
        bindValueBytecodeGenerator.storeField(className, fieldName, fieldDescriptor);
        bindValueBytecodeGenerator.returnVoid();

        bindValueBytecodeGenerator.markLabel(nextNameLabel);
        bindValueBytecodeGenerator.markFrameSame();
    }

    private void extendBindValueForMethod() {
        final Label nextNameLabel = new Label();

        bindValueBytecodeGenerator.pushConstant(bindsName);
        bindValueBytecodeGenerator.loadReference(1);
        bindValueBytecodeGenerator.invokeVirtual(String.class, "equals", boolean.class, Object.class);
        bindValueBytecodeGenerator.jumpEquals(nextNameLabel);

        bindValueBytecodeGenerator.loadReference(0);
        bindValueBytecodeGenerator.loadReference(2);
        bindValueBytecodeGenerator.invokeVirtual(className, methodName, methodDescriptor);
        bindValueBytecodeGenerator.returnVoid();

        bindValueBytecodeGenerator.markLabel(nextNameLabel);
        bindValueBytecodeGenerator.markFrameSame();
    }

    private void finishBindValue() {
        getValueTypeBytecodeGenerator.newReference(BinderException.class);
        getValueTypeBytecodeGenerator.duplicateReference();

        getValueTypeBytecodeGenerator.newReference(StringBuilder.class);
        getValueTypeBytecodeGenerator.duplicateReference();
        getValueTypeBytecodeGenerator.pushConstant("There is no bindable property with name '");
        getValueTypeBytecodeGenerator.invokeConstructor(StringBuilder.class, String.class);
        getValueTypeBytecodeGenerator.loadReference(1);
        getValueTypeBytecodeGenerator.invokeVirtual(StringBuilder.class, "append", StringBuilder.class, String.class);
        getValueTypeBytecodeGenerator.pushConstant(String.format("' in class '%s'.", className.replace('/', '.')));
        getValueTypeBytecodeGenerator.invokeVirtual(StringBuilder.class, "append", StringBuilder.class, String.class);
        getValueTypeBytecodeGenerator.invokeVirtual(StringBuilder.class, "toString", String.class);

        getValueTypeBytecodeGenerator.invokeConstructor(BinderException.class, String.class);
        getValueTypeBytecodeGenerator.throwReference();

        bindValueBytecodeGenerator.endCode(5, 3);
        bindValueBytecodeGenerator.endMethod();
    }
}
